feat(think): add experimental Computer workspace - #2002
Conversation
🦋 Changeset detectedLatest commit: cc8918b The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
agents
@cloudflare/ai-chat
@cloudflare/codemode
create-think
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
Computer now applies its Think filesystem compatibility methods to local and remote clients when the owner enables useThink. Keeping the same translations in Think creates two implementations that can drift and hides the native Computer client behind an adapter object. Make ComputerWorkspace an intersection of WorkspaceClient and WorkspaceLike, and reduce adaptComputer() to a runtime assertion that narrows the owner's runtime configuration. The client remains unchanged, so its filesystem, shell, git, artifact, and asset surfaces stay directly available. Require @cloudflare/computer 0.1.0-alpha.2 as the optional peer. Use the Computer pull request preview as the development dependency until that alpha is published, so fresh installs test the new client contract without a local package link.
|
@aron-cf now that Computer #26 and workspace #24 have merged, could you publish "@cloudflare/computer 0.1.0-alpha.2" from main? Once that's on npm I can swap the PR preview dep, regenerate lockfile, and undraft this PR. |
| function getWorkspaceShell( | ||
| workspace: WorkspaceLike | ||
| ): WorkspaceShellExecutor | undefined { | ||
| try { | ||
| const candidate = workspace as WorkspaceLike & | ||
| WorkspaceWithShellCapability & { | ||
| runtime?: WorkspaceShellExecutor; | ||
| }; | ||
| return candidate[workspaceShellCapability] ?? candidate.runtime; | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Shell-style command tool can be swapped in for workspaces that cannot actually run commands
Any workspace object that merely has a runtime property is treated as able to run shell commands (candidate.runtime at packages/think/src/tools/workspace.ts:247) without checking that it can actually execute anything, so the assistant's command tool can be routed to something that cannot run commands and every command fails.
Impact: Users of a Computer workspace that has no shell execution configured lose the working command tool entirely instead of silently keeping the existing sandboxed fallback.
Duck-typed capability detection bypasses the just-bash fallback
getWorkspaceShell() (packages/think/src/tools/workspace.ts:239-251) returns candidate[workspaceShellCapability] ?? candidate.runtime with no validation that the returned value exposes a callable exec. createWorkspaceTools() (packages/think/src/tools/workspace.ts:220-234) then unconditionally picks createWorkspaceShellTool over createBashTool whenever that value is truthy.
A @cloudflare/computer workspace always exposes runtime, even when no WorkerShellBackend was passed to createComputerWorkspace(). The documentation added in this PR states the Computer shell path applies "When a WorkerShellBackend is configured" (docs/think/tools.md:135-136), implying the just-bash fallback otherwise, but the code never falls back once runtime exists. The same applies to any custom WorkspaceLike that happens to define an unrelated runtime property.
A typeof shell?.exec === "function" guard (and ideally a check that a shell backend exists) would restore the intended fallback.
| function getWorkspaceShell( | |
| workspace: WorkspaceLike | |
| ): WorkspaceShellExecutor | undefined { | |
| try { | |
| const candidate = workspace as WorkspaceLike & | |
| WorkspaceWithShellCapability & { | |
| runtime?: WorkspaceShellExecutor; | |
| }; | |
| return candidate[workspaceShellCapability] ?? candidate.runtime; | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| function getWorkspaceShell( | |
| workspace: WorkspaceLike | |
| ): WorkspaceShellExecutor | undefined { | |
| try { | |
| const candidate = workspace as WorkspaceLike & | |
| WorkspaceWithShellCapability & { | |
| runtime?: WorkspaceShellExecutor; | |
| }; | |
| const shell = candidate[workspaceShellCapability] ?? candidate.runtime; | |
| return typeof shell?.exec === "function" ? shell : undefined; | |
| } catch { | |
| return undefined; | |
| } | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (options.bash !== false) { | ||
| tools.bash = createBashTool({ | ||
| ...(typeof options.bash === "object" ? options.bash : {}), | ||
| ops: workspaceBashOps(workspace) | ||
| }); | ||
| const bashOptions = | ||
| typeof options.bash === "object" ? options.bash : undefined; | ||
| const shell = getWorkspaceShell(workspace); | ||
| tools.bash = shell | ||
| ? createWorkspaceShellTool({ | ||
| shell, | ||
| timeout: bashOptions?.timeout, | ||
| maxOutputBytes: bashOptions?.maxOutputBytes | ||
| }) | ||
| : createBashTool({ | ||
| ...bashOptions, | ||
| ops: workspaceBashOps(workspace) | ||
| }); | ||
| } |
There was a problem hiding this comment.
🟡 Command-tool safety settings such as disabling network access are silently ignored on the new workspace
Configured limits for the command tool — network access and file snapshot caps — are dropped when the new workspace path is used (only timeout and maxOutputBytes are forwarded at packages/think/src/tools/workspace.ts:224-229), so a setting that was meant to keep commands offline has no effect.
Impact: An agent explicitly configured to run commands without network access can still reach the network after opting into the experimental workspace.
Dropped options in the Computer shell branch
BashToolOptions supports timeout, network, maxWorkspaceFiles, maxWorkspaceFileBytes, and maxOutputBytes (packages/think/src/tools/workspace.ts:1153-1160). Think forwards the user's workspaceBash object straight through (packages/think/src/think.ts:5600-5602).
In the new branch only timeout and maxOutputBytes are passed to createWorkspaceShellTool; network in particular is silently discarded, whereas the just-bash path defaults to network disabled and only enables it when network: true. Either the shell path should honor/translate these options, or createWorkspaceTools should surface an error/warning when unsupported options are supplied so the difference is not silent.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (options.bash !== false) { | ||
| tools.bash = createBashTool({ | ||
| ...(typeof options.bash === "object" ? options.bash : {}), | ||
| ops: workspaceBashOps(workspace) | ||
| }); | ||
| const bashOptions = | ||
| typeof options.bash === "object" ? options.bash : undefined; | ||
| const shell = getWorkspaceShell(workspace); | ||
| tools.bash = shell | ||
| ? createWorkspaceShellTool({ | ||
| shell, | ||
| timeout: bashOptions?.timeout, | ||
| maxOutputBytes: bashOptions?.maxOutputBytes | ||
| }) | ||
| : createBashTool({ | ||
| ...bashOptions, | ||
| ops: workspaceBashOps(workspace) | ||
| }); | ||
| } |
There was a problem hiding this comment.
🟨 Network isolation setting for the built-in command tool is dropped on the experimental Computer workspace
When the workspace exposes a native shell, createWorkspaceTools() builds the bash tool through createWorkspaceShellTool and forwards only timeout and maxOutputBytes (packages/think/src/tools/workspace.ts:224-229). The network option from workspaceBash (packages/think/src/think.ts:5600-5602) is silently discarded. In the existing just-bash path network access is disabled unless network: true is set; on the new path model-authored shell scripts run with whatever egress the Computer shell backend allows, with no way to restrict it. Model-generated commands are untrusted input, so this weakens the sandbox default for anyone opting into the experimental workspace.
Was this helpful? React with 👍 or 👎 to provide feedback.
This PR adds an opt-in
@cloudflare/think/experimental/computerentrypoint for using Cloudflare Computer as a Think workspace. Existing Think agents continue to use@cloudflare/shellandjust-bashunless they opt in. It consumes the shared client compatibility added by cloudflare/computer#26.Why
just-bashfallback.@cloudflare/shelltables and Computervfs_*tables.useThinkcompatibility surface. Keeping a second filesystem adapter in Think would duplicate path, stat, and missing-file translations and allow the two implementations to drift.Public API Surface
All additions are under the new experimental export.
createComputerWorkspaceuseThink: trueadaptComputergetWorkspace()client without wrapping itComputerWorkspaceWorkspaceLikeLocalComputerWorkspaceWorkspaceLikeComputerWorkspaceclassgetWorkspacewithWorkspaceWorkspaceServiceProxyWorkerShellBackendComputerOptionscreateComputerWorkspaceWorkspaceClientWorkerShellBackendOptionsWorkerShellFetcherArchitectural Changes
Code Changes
adaptComputer()a runtime assertion because the owner-sideuseThinkoption cannot be inferred across RPC. It returns the original client rather than a compatibility wrapper.createWorkspaceTools()to use a workspace native shell when available while retaining the existingjust-bashfallback.WorkspaceServiceProxyobtain the stub for a Computer owned by the Think Durable Object.Compatibility
just-bashexecution remain unchanged.@cloudflare/computer@>=0.1.1 <0.2.0.@cloudflare/computer@^0.1.1release, which includes the shared Think compatibility surface from computer: Carry Think methods onto clients computer#26.